package cn.remex.configuration.spring;

import cn.remex.configuration.etcd.EtcdConfigCenter;
import cn.remex.core.exception.ServiceCode;
import cn.remex.core.reflect.ReflectUtil;
import cn.remex.core.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * Disc:
 * Created by xinxinran on 2017/7/27.
 */
public class ConfigValueAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
        implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {

    private static final Logger logger= LoggerFactory.getLogger(ConfigValueAnnotationBeanPostProcessor.class);

    private final Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<>(512);

    private ConfigurableListableBeanFactory beanFactory;

    private String requiredParameterName = "required";

    private boolean requiredParameterValue = true;

    /**
     * Callback that supplies the owning factory to a bean instance.
     * <p>Invoked after the population of normal bean properties
     * but before an initialization callback such as
     * {@link InitializingBean#afterPropertiesSet()} or a custom init-method.
     *
     * @param beanFactory owning BeanFactory (never {@code null}).
     *                    The bean can immediately call methods on the factory.
     * @throws BeansException in case of initialization errors
     * @see BeanInitializationException
     */
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
            throw new IllegalArgumentException(
                    "ConfigValueAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory");
        }
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

    /**
     * Post-process the given merged bean definition for the specified bean.
     *
     * @param beanDefinition the merged bean definition for the bean
     * @param beanType       the actual type of the managed bean instance
     * @param beanName       the name of the bean
     */
    @Override
    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
        if (beanType != null) {
            InjectionMetadata metadata = findConfigValueMetadata(beanName, beanType, null);
            metadata.checkConfigMembers(beanDefinition);
        }
    }

    @Override
    public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {

        InjectionMetadata metadata = findConfigValueMetadata(beanName, bean.getClass(), pvs);
        try {
            metadata.inject(bean, beanName, pvs);
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
    }

    /**
     * Get the order value of this object.
     * <p>Higher values are interpreted as lower priority. As a consequence,
     * the object with the lowest value has the highest priority (somewhat
     * analogous to Servlet {@code load-on-startup} values).
     * <p>Same order values will result in arbitrary sort positions for the
     * affected objects.
     *
     * @return the order value
     * @see #HIGHEST_PRECEDENCE
     * @see #LOWEST_PRECEDENCE
     */
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1;
    }

    private InjectionMetadata findConfigValueMetadata(String beanName, Class<?> clazz, PropertyValues pvs) {
        // Fall back to class name as cache key, for backwards compatibility with custom callers.
        String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
        // Quick check on the concurrent map first, with minimal locking.
        InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized (this.injectionMetadataCache) {
                metadata = this.injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }
                    try {
                        metadata = buildConfigValueMetadata(clazz);
                        this.injectionMetadataCache.put(cacheKey, metadata);
                    }
                    catch (NoClassDefFoundError err) {
                        throw new IllegalStateException("Failed to introspect bean class [" + clazz.getName() +
                                "] for autowiring metadata: could not find class that it depends on", err);
                    }
                }
            }
        }
        return metadata;
    }

    private InjectionMetadata buildConfigValueMetadata(final Class<?> clazz) {
        LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<>();
        Class<?> targetClass = clazz;

        do {
            final LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<>();

            ReflectionUtils.doWithLocalFields(targetClass, field -> {
                AnnotationAttributes ann = AnnotatedElementUtils.getMergedAnnotationAttributes(field, ConfigValue.class);
                if (ann != null) {
                    if (Modifier.isStatic(field.getModifiers())) {
                        if (logger.isWarnEnabled()) {
                            logger.warn("ConfigValue annotation is not supported on static fields: " + field);
                        }
                        return;
                    }
                    if (!ReflectUtil.isSimpleType(field.getType()) && !Map.class.isAssignableFrom(field.getType())) {
                        if (logger.isWarnEnabled()) {
                            logger.warn("ConfigValue annotation is only supported on [SimpleType] or [MapType] fields: " + field);
                        }
                        return;
                    }
                    boolean required = (!ann.containsKey(this.requiredParameterName) || this.requiredParameterValue == ann.getBoolean(this.requiredParameterName));
                    currElements.add(new ConfigValueFieldElement(field, ann.getString("value"), required));
                }
            });

            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);

        return new InjectionMetadata(clazz, elements);
    }

    private class ConfigValueFieldElement extends InjectionMetadata.InjectedElement {

        private final boolean required; // 预留

        private final String configKey;

        ConfigValueFieldElement(Field field, String configKey, boolean required) {
            super(field, null);
            this.required = required;
            this.configKey = configKey;

            Assert.notNullAndEmpty(this.configKey, ServiceCode.ERROR, "ConfigValue needs a non-empty value for searching key...");
        }

        /**
         * Either this or {@link #getResourceToInject} needs to be overridden.
         *
         * @param target 注入对象
         * @param requestingBeanName 对应的beanName
         * @param pvs 暂时用不到
         */
        @Override
        protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
            Field field = (Field) this.member;

            DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
            desc.setContainingClass(target.getClass());

            Class<?> type = desc.getDependencyType();
            TypeConverter typeConverter = beanFactory.getTypeConverter();

            List<String> etcdKeys = EtcdConfigCenter.getEtcdKeysFromPlaceHolder(this.configKey);
            List<String> etcdWatchingKeys = etcdKeys.stream().filter(EtcdConfigCenter::canWatchEtcdKey).collect(Collectors.toList());

            String resolvedValue = beanFactory.resolveEmbeddedValue(this.configKey);

            while(EtcdConfigCenter.isEtcdKey(resolvedValue)){
                if(EtcdConfigCenter.canWatchEtcdKey(resolvedValue)){
                    etcdWatchingKeys.add(resolvedValue);
                }
                resolvedValue = beanFactory.resolveEmbeddedValue("${" + resolvedValue + "}");
            }

            if(etcdWatchingKeys.size() > 0){
                etcdWatchingKeys.forEach(wKey -> EtcdConfigCenter.registerWatching(EtcdConfigCenter.getEtcdKey(wKey), newValue -> {
                    try{
                        if (newValue != null) {
                            Object finalValue = typeConverter.convertIfNecessary(newValue, type, field);
                            ReflectionUtils.makeAccessible(field);
                            field.set(target, finalValue);
                        }
                    }catch (Exception e){
                        logger.warn("[ConfigValue::watching] an error occurred while changing new value for " + field.getName(), e);
                    }
                }));
            }

            Object finalValue = typeConverter.convertIfNecessary(resolvedValue, type, field);
            if (finalValue != null) {
                ReflectionUtils.makeAccessible(field);
                field.set(target, finalValue);
            }
        }
    }
}
